#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import json
import locale
import os
import platform
import re
import shutil
import stat
import subprocess
import time

from oracle import logger
from common.cleaner import clear
from common.common import retry_when_exception, execute_cmd, read_result_file
from common.const import CMDResult
from common.exception.common_exception import ErrCodeException
from common.file_common import delete_file
from common.parse_parafile import get_env_variable
from oracle.common.constants import OracleInstallType, WindowsRegistryParentDirectory, ScriptExitCode, \
    ORACLE_SQLPLUS_ERROR_MAP, SliceConstant, PluginPathConstant, RETURN_INVALID, TIME_FORMAT, \
    ERROR_SCRIPT_EXEC_FAILED, WindowsRegistryConstantString, ErrorCode, EnvVariableKeys

if platform.system().lower() == "windows":
    import winreg
    import win32security
    import win32con
    import pywintypes


def execute_mount_bind_windows(cmd):
    try:
        subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except FileNotFoundError:
        logger.info(f"Execute mount bind command failed!")
        return False
    return True


def check_oracle_install_type():
    """
    检查Oracle下载类型
    :return: 返回下载类型（int: single = 0, RAC = 1, single_ASM = 2）
    """
    db_is_cluster = OracleInstallType.DEFAULT
    ret, cluster_flag = get_registry("HKEY_LOCAL_MACHINE", r"SOFTWARE\ORACLE\OCR", "LOCAL_ONLY")
    if not ret:
        logger.warning("Unknown oracle type: %s set default type single 0", cluster_flag)
    elif cluster_flag.upper() == "FALSE":
        db_is_cluster = OracleInstallType.RAC  # oracle rac
    elif cluster_flag.upper() == "TRUE":
        db_is_cluster = OracleInstallType.RESTART_SINGLE_PLUS_ASM  # oracle restart(single + ASM)
    else:
        logger.warning("Unknown oracle type: %s, set default type single 0", cluster_flag)
    logger.info("Check oracle type [db_is_cluster = %s] success", db_is_cluster)
    return db_is_cluster


def check_sqlplus_status():
    cmd = "where sqlplus"
    return_code, std_out, std_err = execute_cmd(cmd)
    if return_code != CMDResult.SUCCESS.value:
        logger.error("Oracle sqlplus is not installed.")
        return False

    logger.info("Oracle sqlplus is installed.")
    return True


def check_rman_status():
    cmd = "where rman"
    return_code, std_out, std_err = execute_cmd(cmd)
    if return_code != CMDResult.SUCCESS.value:
        logger.error("Oracle rman is not installed.")
        return False

    logger.info("Oracle rman is installed.")
    return True


def get_oracle_version(pid):
    # 切入低权限用户
    ret, low_auth_user = windows_switch_user(pid)
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    if ret != ScriptExitCode.SUCCESS:
        logger.error("switch low auth user of windows failed. pid = %s", pid)
        raise ErrCodeException(ScriptExitCode.ERROR_INVALID_LOW_AUTH_INFO,
                               message="switch low auth user of windows failed.")
    try:
        sqlplus_cmd = ['sqlplus', '/nolog', "exit"]
        output = subprocess.check_output(sqlplus_cmd)
        ora_version = ""
        for line in output.splitlines():
            line = line.decode(locale.getdefaultlocale()[1])
            if "SQL*Plus" in line:
                ora_version = line.split(" ")[2]
                break

        ora_pre_version = ora_version[0:2]
        ora_pre_version_extend = ora_version[0:4].replace('.', '')

        logger.info(f"Get oracle version({ora_version}), preVersion({ora_pre_version}), "
                    f"preVersionExtend({ora_pre_version_extend}) success.")
        return [ora_version, ora_pre_version, ora_pre_version_extend]
    finally:
        # 切出登录用户
        windows_switch_user_end(low_auth_user)


def add_unix_timestamp(date):
    time_array = time.strptime(date, "%Y-%m-%d_%H:%M:%S")
    timestamp = time.mktime(time_array)

    return int(timestamp)


def move_pfile_to_stmp(pid, main_backup_path, db_name):
    logger.info(f"Get database pfile parameter, pid:{pid}.")
    fil_path = f"{main_backup_path}\\ebackup-{db_name}-pfile.ora"
    if not os.path.exists(fil_path):
        logger.error(f"Pfile is not exit, pid: {pid}.")
        return RETURN_INVALID
    dest = shutil.copy(fil_path, f"{PluginPathConstant.WINDOWS_TMP_PATH}/tempfile_{pid}")
    if dest != f"{PluginPathConstant.WINDOWS_TMP_PATH}/tempfile_{pid}":
        logger.error(f"Copy pfile is error, pid:{pid}.")
        return RETURN_INVALID

    return ScriptExitCode.SUCCESS


@retry_when_exception(retry_times=3, delay=3)
def create_dir_recursive(file_path):
    os.makedirs(file_path, exist_ok=True)


@retry_when_exception(retry_times=3, delay=3)
def create_empty_file(file_path):
    flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as file:
        os.utime(file_path, None)


def create_asm_dir(pid, tmp_dir, child_paths, asm_instance, oracle_home):
    for child_path_name in child_paths:
        tmp_dir = f"{tmp_dir}/{child_path_name}"
        ret = check_path_in_disk_group(pid, tmp_dir, asm_instance, oracle_home)
        if not ret:
            cmd = f"cmd /c asmcmd mkdir {tmp_dir}"
            os.environ["ORACLE_SID"] = asm_instance
            os.environ["ORACLE_HOME"] = oracle_home
            os.environ["NLS_LANG"] = "American_America.US7ASCII"
            ret, result = execute_windows_cmd(pid, cmd.split(), "")
            if not ret:
                logger.error(f"Asmcmd create dir failed, path is {tmp_dir}, pid:{pid}")
                return False
    return True


def create_dir(pid, dir_path, asm_instance, oracle_home):
    """
    在本地磁盘或ASM磁盘中创建目录
    :pid: 任务id
    :dir_path: 创建目录路径
    :asm_instance: ASM实例名
    :oracle_home: oracle_home路径
    """
    if not dir_path:
        logger.info(f"The path is empty, pid:{pid}.")
        return True
    dir_path_arr = dir_path.split(";")
    for dir_item in dir_path_arr:
        if dir_item[0].isalpha():
            create_dir_recursive(dir_path)
        elif dir_item[0] == "+":
            # get child path name eg: +data/orcl/datafile  return orcl/datafile
            dir_parts = dir_item.split("/")
            dg_name = dir_parts[0]
            dir_length = len(dir_item)
            dg_length = len(dg_name)
            dg_name = dg_name[1:]
            ret = check_disk_group_exits(pid, dg_name, asm_instance, oracle_home)
            if not ret:
                logger.error(f"Asm disk group {dg_name} not exits, pid:{pid}")
                return False
            if dir_length == dg_length:
                return True
            # get child path name eg: +data/orcl/datafile  return orcl/datafile
            ret_code = create_asm_dir(pid, dg_name, dir_parts[1:], asm_instance, oracle_home)
            if not ret_code:
                return ret_code
    logger.info(f"Create direct:{dir_path} success, pid:{pid}")
    return True


def check_disk_group_exits(pid, dg_name, asm_instance, oracle_home):
    if not dg_name or not asm_instance:
        logger.error(f"Param is invalid dg name or asm instance is none, pid:{pid}")
        return False
    dg_name = dg_name.upper()
    os.environ["ORACLE_SID"] = asm_instance
    os.environ["ORACLE_HOME"] = oracle_home
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    cmd = f"cmd /c asmcmd ls {dg_name}"
    ret, result = execute_windows_cmd(pid, cmd.split(), "")
    if not result:
        logger.info(f"Check dist group not exist, pid:{pid}.")
        return False

    logger.info(f"Check dist group exist, pid:{pid}.")
    return True


def check_path_in_disk_group(pid, path, asm_instance, oracle_home):
    """
    检查路径是否在ASM磁盘中
    :pid: 任务id
    :path: 受检查的目录路径
    :asm_instance: ASM实例名
    :oracle_home: oracle_home路径
    """
    if not path or not asm_instance:
        logger.error(f"Param is invalid path name or asm instance is none, pid:{pid}")
        return False
    cmd = f"cmd /c asmcmd ls {path}"
    os.environ["ORACLE_SID"] = asm_instance
    os.environ["ORACLE_HOME"] = oracle_home
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    ret, result = execute_windows_cmd(pid, cmd.split(), "")
    if not ret:
        logger.info(f"Check path is not in disk group, pid: {pid}.")
        return False

    logger.info(f"Check path is in disk group, pid: {pid}.")
    return True


def get_registry(path, sub_path, key_name):
    """
    获取windows指定注册表的值
    :param path: 主路径，以HKEY开头，如HKEY_LOCAL_MACHINE
    :param sub_path: 子路径
    :param key_name: 注册表键
    :return: 返回获取注册表是否成功，以及注册表的值
    """
    path_dict = {
        WindowsRegistryParentDirectory.HKEY_CLASSES_ROOT: winreg.HKEY_CLASSES_ROOT,
        WindowsRegistryParentDirectory.HKEY_CURRENT_USER: winreg.HKEY_CURRENT_USER,
        WindowsRegistryParentDirectory.HKEY_LOCAL_MACHINE: winreg.HKEY_LOCAL_MACHINE,
        WindowsRegistryParentDirectory.HKEY_USERS: winreg.HKEY_USERS,
        WindowsRegistryParentDirectory.HKEY_CURRENT_CONFIG: winreg.HKEY_CURRENT_CONFIG
    }
    parent_path = path_dict.get(path, "")
    try:
        target_key = winreg.OpenKeyEx(parent_path, sub_path)
        target_key_value = winreg.QueryValueEx(target_key, key_name)
    except FileNotFoundError:
        logger.warning("Registry %s\\%s\\%s does not exist!", path, sub_path, key_name)
        return False, ""
    return True, target_key_value[0]


def list_registry_directory(path):
    """
    列出给定注册表下的所有子目录
    :param path: (str)给定的注册表目录
    :return: 返回给定目录下的全部子目录(list)，包含给定目录
    """
    registry_list = []
    try:
        key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, path, 0, winreg.KEY_READ)
        i = 0
        while True:
            subkey = winreg.EnumKey(key, i)
            subpath = path + "\\" + subkey
            list_registry_directory(subpath)
            i += 1
            registry_list.append(subpath)
    except WindowsError:
        pass
    return registry_list


def execute_windows_cmd(pid, cmd_list: list, input_cmd: str, cd_path=""):
    """
    在windows中执行cmd并获取其打印信息
    :param pid: 请求id
    :param cmd_list: 将要执行的cmd命令，注意该入参为列表，将要执行的命令以空格为间隔分开
    :param input_cmd: 将要与命令行进行交互的输入，为空则无交互命令；当要进行多次交互时，以\n进行分割
    :param cd_path: 在执行cmd命令最开始想要切换的路径，为空则不切换路径
    :return: 返回执行命令行是否成功，以及其打印的信息
    """
    result = ""
    encoding = locale.getdefaultlocale()[1]
    if (not os.path.isdir(cd_path)) and cd_path:
        logger.error("Path: %s cannot be found!", cd_path)
        return False, result
    elif cd_path:
        os.chdir(cd_path)
    try:
        logger.info(f"cmd:{cmd_list}")
        process = subprocess.Popen(cmd_list, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    except FileNotFoundError:
        logger.error("Execute command failed! Check if the command is correct or not! Pid = %s", pid)
        return False, result
    if input_cmd:
        process.stdin.write(input_cmd.encode())
    output, error = process.communicate(timeout=None)

    if error:
        logger.error("Execute windows command failed! pid = %s; error info: %s", pid, error.decode(encoding=encoding))
        return False, result
    result = output.decode(encoding=encoding)
    logger.info("Execute windows command succeed! pid = %s", pid)
    return True, result


def execute_windows_cmd_with_low_auth_user(pid, cmd_list: list, input_cmd: str, cd_path=""):
    """
    在windows中执行cmd并获取其打印信息
    :param pid: 请求id
    :param cmd_list: 将要执行的cmd命令，注意该入参为列表，将要执行的命令以空格为间隔分开
    :param input_cmd: 将要与命令行进行交互的输入，为空则无交互命令；当要进行多次交互时，以\n进行分割
    :param cd_path: 在执行cmd命令最开始想要切换的路径，为空则不切换路径
    :return: 返回执行命令行是否成功，以及其打印的信息
    """
    ret, low_auth_user = windows_switch_user(pid)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("switch low auth user of windows failed. pid = %s", pid)
        raise ErrCodeException(ScriptExitCode.ERROR_INVALID_LOW_AUTH_INFO,
                               message="switch low auth user of windows failed.")
    try:
        return execute_windows_cmd(pid, cmd_list, input_cmd, cd_path)
    finally:
        windows_switch_user_end(low_auth_user)


def execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name):
    """
    在windows中执行sqlplus指令，输出结果文件，输出错误具体详情文件，输出错误码
    错误具体详情文件在stmp路径下，以result_errordetail_{pid}的形式保存
    :param pid: 请求id
    :param params: 包括"is_silence"(int: 0, 1)，"db_user"(str)和"db_password"(str)三个字段的字典
    :param tmp_sql: 执行命令语句的.sql文件路径
    :param tmp_rst: 输出的结果文件路径，该路径由tmp_sql里的内容决定
    :param instance_name: 数据库实例名
    :return: 执行语句的错误码信息，0为成功（不输出错误具体详情文件）
    """
    # 解析params信息
    is_silence = params.get("is_silence", 0)
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    silent_mode = ""
    if is_silence == 1:
        silent_mode = "-S"

    if db_password:
        db_password = f'"{db_password}"'

    # 执行命令
    os.environ["ORACLE_SID"] = instance_name
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    cmd_list = f'sqlplus /nolog'.split()
    input_cmd = f'conn {db_user}/{db_password} as sysdba\n' \
                f'@{tmp_sql}\n'

    ret, result = execute_windows_cmd_with_low_auth_user(pid, cmd_list, input_cmd)

    # 输出错误码以及错误详情文件
    if not ret or ("ERROR" in result):
        logger.error("Execute oracle sqlplus cmd failed. pid = %s", pid)
        result_code = ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED
        err_code = get_sql_error_info(pid, tmp_rst, "sqlplus", result_code)
        return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
    return ScriptExitCode.SUCCESS


def execute_windows_rman_cmd(pid, params, enc_type, sql_file, rst_file):
    """
    在windows中执行rman指令，输出结果文件，输出错误具体详情文件，输出错误码
    错误具体详情文件在stmp路径下，以result_errordetail_{pid}的形式保存
    :param pid: 请求id
    :param params: 包括"is_enc_bk"(int: 0, 1)，"instance name"(str)，
                            "db_user"(str)，"db_password"(str)和"rman_enc_section"五个字段的字典
    :param enc_type: enc_type
    :param sql_file: 执行命令语句的.sql文件路径
    :param rst_file: 输出的结果文件路径
    :return: 执行语句的错误码信息，0为成功（不输出错误具体详情文件）
    """
    # 解析params信息
    is_enc_bk = params.get("is_enc_bk", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")
    rman_enc_section = params.get("rman_enc_section", "")

    if is_enc_bk != 0 and enc_type == 1:
        encoding = locale.getdefaultlocale()[1]
        origin_backup_sql = read_result_file(sql_file, encoding=encoding)
        rman_enc_section += origin_backup_sql
        write_tmp_file(sql_file, rman_enc_section)

    # 执行命令
    if db_password:
        db_password = f'"{db_password}"'

    os.environ["ORACLE_SID"] = instance_name
    os.environ["NLS_LANG"] = "American_America.US7ASCII"

    cmd_list = f"rman target {db_user}/ cmdfile {sql_file} log {rst_file}".split()
    ret, result = execute_windows_cmd(pid, cmd_list, db_password)

    # 输出错误码以及错误详情文件
    flags_read = os.O_RDONLY
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(rst_file, flags_read, modes), "r") as fout:
        content = fout.read()
    if (not ret) or ("ERROR" in content):
        logger.error(f"Execute oracle rman cmd failed! pid = %s", pid)
        result_code = ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED
        err_code = get_sql_error_info(pid, rst_file, "rman", result_code)
        return ScriptExitCode.ERROR_ORACLE_EXERMAN_FAILED
    return ScriptExitCode.SUCCESS


def get_sql_error_info(pid, result_file, cmd_type, result_code):
    """
    从结果文件中分析错误信息
    :param pid: 请求id
    :param result_file: 待分析的结果文件路径
    :param cmd_type: 待分析的结果文件所执行的命令种类：sqlplus 或 rman
    :param result_code: 默认错误码，若为分析出错误信息，则返回该错误码
    :return: 输出错误详情文件（在stmp中以result_errordetail_{pid}的形式呈现），并返回错误码
    """
    # 读取结果文件
    flags = os.O_RDONLY
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(result_file, flags, modes), "r") as fout:
        error_info_content = fout.read()

    # 从错误码map中查找是否有对应的错误信息
    for key in ORACLE_SQLPLUS_ERROR_MAP.keys():
        if key in error_info_content:
            error_detail = ORACLE_SQLPLUS_ERROR_MAP.get(key, "").get("detail", "")
            error_code = ORACLE_SQLPLUS_ERROR_MAP.get(key, "").get("code", "")
            logger.error("Oracle sqlplus execute result file find error, %s. pid = %s", error_detail, pid)
            write_error_info_to_file(pid, result_file)
            return error_code

    error_info_list = error_info_content.splitlines()
    error_code_list = ["ORA-28002", "ORA-32004", "ORA-32006", "ORA-16136"]
    for line in error_info_list:
        if cmd_type == "sqlplus":
            if check_error_code(line, error_code_list):
                logger.error("Oracle exec sqlplus script failed. pid = %s", pid)
                write_error_info_to_file(pid, result_file)
                return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
        else:
            if line.startswith("RMAN", SliceConstant.HEAD_OF_STRING_OR_LIST):
                logger.error("Oracle exec rman script failed. pid = %s", pid)
                write_error_info_to_file(pid, result_file)
                return ScriptExitCode.ERROR_ORACLE_EXERMAN_FAILED
    return result_code


def check_error_code(text, error_code_list):
    """
    查找错误码表中的错误码是否不存在于字符串中，为get_sql_error_info专门调用，其它函数不建议调用
    :param text: 待检查的字符串信息
    :param error_code_list: 错误码表
    :return: 如果错误码表的所有错误码都不存在于text中，且text以ORA-开头，则返回True；否则返回False
    """
    flag = True
    for code in error_code_list:
        if code in text:
            flag = False
            break
    if flag and text.startswith("ORA-", SliceConstant.HEAD_OF_STRING_OR_LIST):
        return True
    return False


def write_error_info_to_file(pid, result_file):
    """
    根据结果文件输出错误详情文件，注意，该函数没有返回值
    :param pid: 请求id
    :param result_file: 待分析的结果文件
    """
    # 检查文件是否存在
    error_detail_file = PluginPathConstant.WINDOWS_TMP_PATH + f"/result_errordetail_{pid}"
    if os.path.isfile(error_detail_file):
        os.remove(error_detail_file)
    if not os.path.isfile(result_file):
        logger.error("Result file not exist. Cannot write error info file. pid = %s", pid)

    # 分析结果文件并输出
    flags = os.O_RDONLY
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(result_file, flags, modes), "r") as fout:
        error_detail_content = fout.read()
        error_detail_list = error_detail_content.splitlines()
    flags_append = os.O_WRONLY | os.O_CREAT
    with os.fdopen(os.open(error_detail_file, flags_append, modes), "a") as fout:
        for line in error_detail_list:
            if ("RMAN-" in line) or ("ORA-" in line):
                fout.write(line + "\n")


def redirect_backup_path(backup_path):
    backup_path_arr = backup_path.split(";")
    tmp_path = backup_path_arr[0]
    if os.path.exists(os.path.join(tmp_path, "data")) and os.path.isdir(os.path.join(tmp_path, "data")):
        tmp_backup_path = ""
        for path in backup_path_arr:
            tmp_backup_path = f"{tmp_backup_path}{path}/data;"

        backup_path = tmp_backup_path
    return backup_path


def get_real_instance_name(params, db_is_cluster, db_name=""):
    db_user = params.get("db_user")
    db_password = params.get("db_password")
    db_instance_name = params.get("instance_name")
    oracle_home = params.get("oracle_home")
    pid = params.get("pid")
    logger.info(f"Function get_real_instance_name start! Pid = {pid}")

    if db_is_cluster == 0 or db_is_cluster == 2:
        logger.info(f"Database is single, real instance is {db_instance_name}. Pid = {pid}")
        return db_instance_name

    os.environ["ORACLE_SID"] = db_instance_name
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    temp_db_name = query_db_name(db_password, oracle_home, db_user, pid)
    if temp_db_name.lower() == db_name.lower():
        # 查询数据库名和给定数据库名相等，则返回对应数据库的实例名
        logger.info(f"Find the instance with the same database name = {db_name}. Pid = {pid}")
        return db_instance_name
    # 如果参数下发的db_instance_name查到的数据库名不对，再用注册表里的ORACLE_SID查数据库名
    oracle_registries = list_registry_directory("SOFTWARE\\ORACLE")
    logger.info(f"Start to iterate registries to find the given database with db_name = {db_name}! pid = {pid}")
    for reg in oracle_registries:
        if reg.startswith("SOFTWARE\\ORACLE\\KEY_OraDB", SliceConstant.HEAD_OF_STRING_OR_LIST) or \
                reg.startswith("SOFTWARE\\ORACLE\\KEY_OraDb", SliceConstant.HEAD_OF_STRING_OR_LIST):
            result = query_actual_instance_name_from_registry(pid, reg, db_user, db_password, db_name)
            if not result:
                logger.warning(f"{reg}, get real instance name FAILED! Pid = {pid}")
                continue
            elif result == "-1":
                logger.info(
                    f"The db_name of the current instance is NOT same with the given db_name. "
                    f"Keep searching. Pid = {pid}")
                continue
            logger.info(f"Found the real instance name SUCCESS! Pid = {pid}")
            return result
    logger.error(f"Does Not find any instance that has the same database name! Pid = {pid}")
    return ""


def query_actual_instance_name_from_registry(pid, reg, db_user, db_password, db_name):
    logger.info(f"Function query_actual_instance_name_from_registry start! Pid = {pid}")
    # 获取oracle_home用于查询数据库名
    ret, oracle_home = get_registry(WindowsRegistryParentDirectory.HKEY_LOCAL_MACHINE, reg, "ORACLE_HOME")
    if not ret:
        logger.error(
            f"Cannot find home_env! Registry path is {WindowsRegistryParentDirectory.HKEY_LOCAL_MACHINE}\\{reg}. "
            f"Pid = {pid}")
        return ""
    ret, oracle_instance_name = get_registry(WindowsRegistryParentDirectory.HKEY_LOCAL_MACHINE, reg,
                                             WindowsRegistryConstantString.ORACLE_SID)
    if not ret:
        logger.error(f"Get oracle instance name FAILED! Pid = {pid}")
        return ""
    # 用获取到的oracle_home、oracle_sid查询数据库名
    os.environ["ORACLE_SID"] = oracle_instance_name
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    temp_db_name = query_db_name(db_password, oracle_home, db_user, pid)
    if temp_db_name.lower() == db_name.lower():
        # 查询数据库名和给定数据库名相等，则返回对应数据库的实例名
        logger.info(f"Get oracle real instance name SUCCESS! {oracle_instance_name} Pid = {pid}")
        return oracle_instance_name
    logger.info(f"The instance has a different database name with the given database. Pid = {pid}")
    return "-1"


def query_db_name(db_password, oracle_home, db_user, pid):
    if db_password:
        db_password = f'"{db_password}"'
    cd_path = f"{oracle_home}\\BIN"
    sqlplus_cmd = f"sqlplus /nolog".split()
    query_db_cmd = f'conn {db_user}/{db_password} as sysdba\n' \
                   f'select name from v$database;'
    ret_code, output = execute_windows_cmd_with_low_auth_user(pid, sqlplus_cmd, query_db_cmd, cd_path)
    if not ret_code:
        logger.error(f"Command of searching database name is FAILED! pid = {pid}")
        return ""
    temp_result_list = output.strip().splitlines()
    temp_db_name = ""
    for i, _ in enumerate(temp_result_list):
        if temp_result_list[i].startswith("NAME", SliceConstant.HEAD_OF_STRING_OR_LIST):
            temp_db_name = temp_result_list[i + SliceConstant.INDEX_DIFFERENCE_BETWEEN_KEY_AND_VALUE].strip()
            break
    return temp_db_name


@retry_when_exception(retry_times=3, delay=3)
def write_tmp_file(file_path, content):
    flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
    modes = stat.S_IWUSR | stat.S_IRUSR | stat.S_IXUSR
    with os.fdopen(os.open(file_path, flags, modes), 'w') as out_file:
        out_file.write(content)


def get_service_full_name(pid, service_name):
    service_cmd = f"SERVICE_NAME: {service_name}"
    service_full_name = ""
    cmd = f"sc query state= all type= service"
    ret, result = execute_windows_cmd(pid, cmd.split(), "")
    result_lines = result.split("\r\n")
    for line in result_lines:
        if line.find(service_cmd) != -1:
            service_full_name = line.split(": ")[1]
            logger.info(f"Success get service full name:{service_full_name}, pid:{pid}.")
            break
    return service_full_name


def get_asm_instance(pid, asm_service):
    asm_instance = ""
    asm_service_full_name = get_service_full_name(pid, asm_service)
    if asm_service_full_name:
        logger.info(f"Service found: {asm_service_full_name}, pid:{pid}.")
    else:
        logger.error(f"Service not found, pid:{pid}.")
        return asm_instance
    asm_instance = asm_service_full_name.split(asm_service)[1]
    logger.info(f"Success get asm instance: {asm_instance}, pid:{pid}.")
    return asm_instance


def set_db_silence_sql(tmp_sql):
    if not tmp_sql or not os.path.isfile(tmp_sql):
        logger.error("Sql file is none or not exist! Set db silence failed!")
        return RETURN_INVALID

    content = f"set pagesize 0\n" \
              f"set feedback off\n" \
              f"set newpage none\n" \
              f"set heading off\n"

    encoding = locale.getdefaultlocale()[1]
    original_content = read_result_file(tmp_sql, encoding=encoding)
    content += original_content
    write_tmp_file(tmp_sql, content)
    return ScriptExitCode.SUCCESS


def get_database_info(pid, instance_name, db_user, db_password):
    logger.info(f"Query database info, pid: {pid}, database instance name: {instance_name}, user name: {db_user}.")
    encoding = locale.getdefaultlocale()[1]
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_info_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_info_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set linesize 999\n" \
              f"COL LOG_MODE FORMAT a20\n" \
              f"COL DB_UNIQUE_NAME FORMAT a20\n" \
              f"select a.LOG_MODE, a.DBID, a.DB_UNIQUE_NAME, a.OPEN_MODE, b.INCARNATION#, b.RESETLOGS_ID from " \
              f"v$database a, v$database_incarnation b where b.STATUS='CURRENT';\n" \
              f"SPOOL OFF\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }
    # 有调用方传递的pid的值不是pid，所以这里使用ppid
    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.info(f"Get database base info failed, pid:{pid}, ret:{ret_code}, "
                    f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code, []
    # 读取结果文件
    content = read_result_file(tmp_rst, encoding=encoding)
    new_content = content.rstrip()
    database_info = new_content.split()

    if len(database_info) > 7:
        open_mode = database_info[3] + database_info[4] + database_info[5] + database_info[6]
        incarnation_number = database_info[7]
        resetlogs_id = database_info[8]
    else:
        open_mode = database_info[3] + database_info[4]
        incarnation_number = database_info[5]
        resetlogs_id = database_info[6]

    # 删除产生的临时文件
    delete_file(tmp_sql)
    delete_file(tmp_rst)

    return ScriptExitCode.SUCCESS, [database_info[0], database_info[1], database_info[2], open_mode, incarnation_number,
                                    resetlogs_id]


def get_datafile_mini_scn(pid, instance_name, db_user, db_password):
    logger.info(f"Get datafile mini scn, pid: {pid}, database instance name: {instance_name}, user name: {db_user}.")
    encoding = locale.getdefaultlocale()[1]
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_backup_scn_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_backup_scn_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"select to_char(max(CHECKPOINT_CHANGE#)) as scn from v$datafile;\n" \
              f"SPOOL OFF;\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.info(f"Get system min data file scn failed, pid:{pid}, ret:{ret_code}, "
                    f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code, ""
    content = read_result_file(tmp_rst, encoding=encoding)
    mini_scn = content.strip()

    logger.info(f"Get system min data file scn is: {mini_scn}, pid:{pid}.")
    # 删除产生的临时文件
    delete_file(tmp_sql)
    delete_file(tmp_rst)

    return ret_code, mini_scn


def get_datafile_info(pid, ora_prevision, db_user, db_password, **kwargs):
    instance_name = kwargs.get('instance_name')
    tmp_rst = kwargs.get('tmp_rst')
    logger.info(f"Get datafile info, pid: {pid}, database instance name: {instance_name}, user name: {db_user}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_datafile_info_{pid}.sql"

    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set linesize 999\n" \
              f"col tsFile for a520\n"
    if int(ora_prevision) > 11:
        sql_cmd += f"SELECT t.CON_ID CON_ID, t.Name tsName, f.File# fNo, f.Name tsFile, f.bytes/1024/1024 fMbSize " \
                   f"FROM V$TABLESPACE t, V$DATAFILE f WHERE t.TS# = f.TS# and t.CON_ID = f.CON_ID order by bytes " \
                   f"desc;\n"
    else:
        sql_cmd += f"SELECT t.TS# ts, t.Name tsName, f.File# fNo, f.Name tsFile, f.bytes/1024/1024 fMbSize " \
                   "FROM V$TABLESPACE t, V$DATAFILE f WHERE t.TS# = f.TS# order by bytes desc;\n"
    sql_cmd += f"SPOOL OFF\n" \
               f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    delete_file(tmp_sql)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.info(
            f"Get database-{instance_name} datafile list failed, pid:{pid}, ret:{ret_code}, "
            f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
    logger.info(f"Get datafile info success, pid:{pid}.")
    return ret_code


def create_pfile_from_content(content, main_backup_path, db_name):
    if not content:
        logger.error("Create pfile from parameter failed, backup failed.")
        return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
    pfile_path = f"{main_backup_path}/ebackup-{db_name}-pfile.ora"
    logger.info(f"Creating pfile: {pfile_path} from parameter content.")
    lines = content.splitlines()
    try:
        with open(pfile_path, 'w') as outfile:
            process_lines(lines, outfile)
    except Exception as ex:
        logger.error(f"Open pfile path failed, err: {ex}.")
        return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
    logger.info(f"Creating pfile: {pfile_path} success.")
    return ScriptExitCode.SUCCESS


def process_lines(lines, outfile):
    i = 0
    while i < len(lines):
        line = lines[i].strip()
        # 跳过无关的行
        if re.match(r'^(SQL\*Plus|Copyright|Connected to:|Oracle Database|Version|Disconnected from)', line):
            i += 1
            continue
        # 匹配 "实例名 参数名" 的行
        if re.match(r'^[\w]+[ \t]+[\w_]+$', line):
            extract_and_write_parameter(outfile, lines, i)
        i += 1


def extract_and_write_parameter(outfile, lines, index):
    parts = lines[index].split()
    # 防止越界
    if len(parts) < 2:
        logger.info(f"Parameter info line length less than 2, content: {parts}.")
        return
    instance_name = parts[0]
    parameter_name = parts[1]
    if index + 1 < len(lines):
        # 获取下一行的参数值
        next_line = lines[index + 1].strip()
        # 确保下一行非空再提取值
        if next_line:
            parameter_value = next_line.split()[0].replace(",", "")
            outfile.write(f"{instance_name}.{parameter_name}='{parameter_value}'\n")
            logger.info(f"Extracted: {instance_name}.{parameter_name}={parameter_value}")


def create_spfile_from_parameter(login_db_params, main_backup_path, db_name):
    pid = login_db_params.get('pid')
    instance_name = login_db_params.get('instance_name')
    db_user = login_db_params.get('db_user')
    db_password = login_db_params.get('db_password')
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_parameter_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_parameter_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set pagesize 0\n" \
              f"set feedback off\n" \
              f"set newpage none\n" \
              f"set heading off\n" \
              f"set linesize 999\n" \
              f"COL name FORMAT a500\n" \
              f"select i.INSTANCE_NAME,p.NAME, p.VALUE from gv$parameter p, gv$instance i where isdefault !='TRUE'" \
              f"and i.INST_ID=p.INST_ID;\n" \
              f"SPOOL OFF;\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }
    logger.info(f"Getting parameter info from database: {db_name} using sql.")
    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.error(f"Get database parameter failed, pid:{pid}, ret_code:{ret_code}, "
                     f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code
    encoding = locale.getdefaultlocale()[1]
    content = read_result_file(tmp_rst, encoding=encoding).strip()
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    ret_code = create_pfile_from_content(content, main_backup_path, db_name)
    return ret_code


def create_pfile_from_spfile(login_db_params, main_backup_path, db_name, start_with_spfile_flag=True):
    pid = login_db_params.get('pid')
    instance_name = login_db_params.get('instance_name')
    db_user = login_db_params.get('db_user')
    db_password = login_db_params.get('db_password')

    logger.info(f"Create pfile from spfile by sqlplus at instance[{instance_name}],pid:{pid}, "
                f"main_backup_path:{main_backup_path}")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/create_pfile_from_spfile_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/create_pfile_from_spfile_rst_{pid}.txt"

    if start_with_spfile_flag:
        sql_cmd = f"SPOOL {tmp_rst}\n" \
                  f"create pfile='{main_backup_path}\\ebackup-{db_name}-pfile.ora' from spfile;\n" \
                  f"SPOOL OFF\n" \
                  f"exit;\n"
        write_tmp_file(tmp_sql, sql_cmd)
        set_db_silence_sql(tmp_sql)
        params = {
            'is_silence': 0,
            'db_user': db_user,
            'db_password': db_password
        }

        ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
        if ret_code != ScriptExitCode.SUCCESS:
            encoding = locale.getdefaultlocale()[1]
            logger.error(f"Create pfile failed, pid:{pid}, ret_code:{ret_code}, "
                         f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
    else:
        ret_code = create_spfile_from_parameter(login_db_params, main_backup_path, db_name)
        if ret_code != ScriptExitCode.SUCCESS:
            encoding = locale.getdefaultlocale()[1]
            logger.error(f"Create pfile failed, pid:{pid}, ret_code:{ret_code}, "
                         f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    if ret_code == ScriptExitCode.SUCCESS:
        logger.info(f"create pfile from spfile success, pid:{pid}.")
    return ret_code


def cross_check_backup_dbf(pid, params, enc_type, db_name):
    logger.info(f"Begin to crosscheck copy datafile, pid:{pid}, db_name: {db_name}.")

    if not db_name:
        logger.error(f"Db name is none can not crosscheck datafile, pid:{pid}.")
        return RETURN_INVALID

    tmp_sql = rf"{PluginPathConstant.WINDOWS_TMP_PATH}/'rman_cross_check_data_{pid}.sql"
    tmp_rst = rf"{PluginPathConstant.WINDOWS_TMP_PATH}/'rman_cross_check_data_{pid}.rst"
    sql_cmd = f"crosscheck datafilecopy tag 'EBACKUP-{db_name.upper()}-DATA';\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)

    ret_code = execute_windows_rman_cmd(pid, params, enc_type, tmp_sql, tmp_rst)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.warning(f"Execute crosscheck copy datafile failed, pid:{pid} ret_code:{ret_code}, "
                       f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code

    logger.info(f"Crosscheck copy datafile successful, pid:{pid}.")
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    return ret_code


def cross_check_archive_log(pid, params, enc_type):
    logger.info(f"Begin to crosscheck archive log, pid:{pid}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/rman_cross_check_log_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/rman_cross_check_log_{pid}.txt"

    sql_cmd = f"crosscheck archivelog all;\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)

    ret_code = execute_windows_rman_cmd(pid, params, enc_type, tmp_sql, tmp_rst)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.warning(f"Execute crosscheck archive_log failed, pid:{pid}, ret_code:{ret_code}, "
                       f"error is:{read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code

    logger.info(f"Crosscheck archive_log successful, pid:{pid}.")
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    return ret_code


def get_file_lines(file_path):
    lines = 0
    with open(file_path, 'r') as file:
        for _ in file:
            lines += 1
    return lines


def find_content_in_file(content, file_path):
    with open(file_path, "r") as file:
        for line in file:
            if content in line:
                return True

    return False


def get_dbf_contents(file_path, pid):
    extend = ".dbf"
    dbf_contents = []
    for dir_path, _, files in os.walk(file_path):
        for name in files:
            if name.lower().endswith(extend):
                file_name = os.path.join(dir_path, name)
                if '.snapshot' not in file_name:
                    dbf_contents.append(file_name)

    logger.info(f"Success get dbf contents: {dbf_contents}, pid:{pid}.")
    return dbf_contents


def build_backup_datafile_sql(full_backup_sql, datafile_info, params):
    create_empty_file(full_backup_sql)
    backup_path_list = params.get("backup_path_list")
    db_name = params.get("db_name")
    level = params.get("level")
    additional = params.get("additional")
    pid = params.get('pid')
    asm_instance = params.get("asm_instance")
    oracle_home = params.get("oracle_home")

    if not full_backup_sql or not os.path.exists(datafile_info):
        logger.error(f"Param is none cannot build backup sql, pid:{pid}.")
        return

    logger.info("Build datafile sql.")
    path_num = len(backup_path_list.split(";"))
    index_datafile = 0
    content = f"    backup as copy incremental level 0 tag 'EBACKUP-{db_name}-DATA' \n"
    init_size = os.path.getsize(full_backup_sql)
    cur_strlen = init_size
    with open(datafile_info, "r") as file:
        line = file.read()
        file_contents = line.splitlines()
        logger.info(f"Datafile info file_content: {file_contents}")

    for line_content in file_contents:
        if "rows selected" in line_content or "row selected" in line_content:
            continue
        db_info_content = line_content.split()
        if len(db_info_content) == 0:
            continue
        f_no, fs_file = db_info_content[2], db_info_content[3]
        result = False
        if level != 0:
            result = find_content_in_file(fs_file, f"{additional}/dbfiles")

        if not result:
            index_path = index_datafile % path_num
            index_datafile += 1
            path = backup_path_list.split(";")[index_path]
            create_dir(pid, f"{path}/{f_no}", asm_instance, oracle_home)
            tmp_str = f"    (datafile {f_no} format '{path}\\{f_no}\\FNO-%f_TS-%N.dbf')\n"
            cur_strlen += len(tmp_str)

            content += f"{tmp_str}\n"

    if cur_strlen == init_size:
        delete_file(full_backup_sql)
    else:
        content += f";\n"
        write_tmp_file(full_backup_sql, content)


def build_backup_database_sql(tmp_sql, datafile_min_scn, params):
    pid = params.get('pid')
    datafile_info = params.get('datafile_info')
    db_name = params.get('db_name')
    level = int(params.get('level'))
    ora_pre_version = int(params.get('ora_pre_version'))
    is_enc_bk = params.get('is_enc_bk')
    qos = params.get('qos')
    channels = params.get('channels')
    db_is_cluster = params.get('db_is_cluster')
    node_number = params.get('node_number')
    ip_port_info = params.get('ip_port_info')
    main_backup_path = params.get('main_backup_path')
    backup_tmp = params.get('backup_tmp')
    open_mode = params.get('open_mode')

    backup_datafile_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/backup_datafile_sql_{pid}.sql"
    build_backup_datafile_sql(backup_datafile_sql, datafile_info, params)

    dbf_num = get_file_lines(datafile_info)
    content = f""
    if ora_pre_version > 12:
        content += f"alter session set events 'trace[krb.*] disk disable, memory disable';\n"
    content += f"configure backup optimization off;\n"
    content += f"configure controlfile autobackup off;\n"
    content += f"set nocfau;\n"
    content += f"configure maxsetsize to unlimited;\n"
    if is_enc_bk == 0 and ora_pre_version > 11:
        content += f"configure encryption for database off;\n"

    content += "RUN {\n"
    content += f"    SET COMMAND ID TO 'ProtectAgent_Backup';\n"
    if level == 0:
        content += f"    change archivelog like '%/epoch-scn_%/arch_%' uncatalog;\n"

    real_qos = 0
    if qos > 0:
        if dbf_num >= channels or dbf_num == 0:
            real_qos = qos * 1024 // channels
        else:
            real_qos = qos * 1024 // dbf_num
    index = 1
    if db_is_cluster == 1 and node_number != 0:
        while index <= channels:
            for instance_node_info in ip_port_info:
                [sid_info, host_info, port_info] = instance_node_info.split(",")
                if qos == 0:
                    content += f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk connect /@(" \
                               f"DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host={host_info})(Port={port_info}))(" \
                               f"CONNECT_DATA=(SID={sid_info})));\n"
                else:
                    content += f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk rate {real_qos}k " \
                               f"connect /@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host={host_info})(Port={port_info}))(" \
                               f"CONNECT_DATA=(SID={sid_info})));\n"

                index += 1
                if index > channels:
                    break
    else:
        while index <= channels:
            if qos == 0:
                content += f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk;\n"
            else:
                content += f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk rate {real_qos}k;\n"
            index += 1

    content += f"    configure device type disk parallelism {channels};\n"
    content += f"    backup spfile format '{main_backup_path}\\spfile.bs' tag 'EBACKUP-{db_name}-SPFILE' reuse;\n"
    if os.path.exists(backup_datafile_sql):
        encoding = locale.getdefaultlocale()[1]
        build_datafile_content = read_result_file(backup_datafile_sql, encoding=encoding)
        delete_file(backup_datafile_sql)
        content += build_datafile_content

    if level == 1 or level == 2:
        dbf_contents = get_dbf_contents(main_backup_path, pid)
        for dbf in dbf_contents:
            content += f"    catalog datafilecopy '{dbf}' tag 'EBACKUP-{db_name}-DATA' level 0;\n"

    if level == 1:
        content += f"    backup incremental level 1 cumulative for recover of copy with tag 'EBACKUP-{db_name}-DATA' " \
                   f"database format '{backup_tmp}\\%T_%U';\n"
        content += f"    recover copy of database with tag 'EBACKUP-{db_name}-DATA';\n"
        content += f"    delete noprompt backup tag 'EBACKUP-{db_name}-DATA';\n"
    elif level == 2:
        content += f"    backup incremental level 1 for recover of copy with tag 'EBACKUP-{db_name}-DATA' database " \
                   f"format '{backup_tmp}\\%T_%U';\n"
        content += f"    recover copy of database with tag 'EBACKUP-{db_name}-DATA';\n"
        content += f"    delete noprompt backup tag 'EBACKUP-{db_name}-DATA';\n"

    log_parent_path = main_backup_path[0:main_backup_path.rfind('\\')]
    content += f"    DELETE FORCE NOPROMPT ARCHIVELOG like '{log_parent_path}\\%\\log\\arch_%';\n".upper()
    if open_mode != 'READONLY' and open_mode != "READONLYWITHAPPLY":
        content += f"    sql 'alter system archive log current';\n"

    content += f"    backup as copy archivelog from scn {datafile_min_scn} format " \
               f"'{main_backup_path}\\log\\arch_%t_%s.log' reuse;\n"
    content += f"    backup as copy current controlfile format '{main_backup_path}\\controlfile.ctl' tag 'EBACKUP-" \
               f"{db_name}-CTL' reuse;\n"
    content += "}\n"

    write_tmp_file(tmp_sql, content)


def get_database_node_list(pid, instance_name, db_user, db_password):
    logger.info(f"Get database node list, pid:{pid}, instance name:{instance_name}, user_name:{db_user}.")
    encoding = locale.getdefaultlocale()[1]
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_node_list_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_node_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set linesize 300;\n" \
              f"select THREAD# from GV$INSTANCE where STATUS='OPEN';\n" \
              f"SPOOL OFF;\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)

    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.info(f"Get database node list failed, pid:{pid}, ret:{ret_code}, "
                    f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code, []
    content = read_result_file(tmp_rst, encoding=encoding)
    node_list = content.strip()
    node_list_arr = node_list.split()
    logger.info(f"Get database node list success, pid:{pid}, ret_code:{ret_code}, node_list_arr:{node_list_arr}.")
    # 删除产生的临时文件
    delete_file(tmp_sql)
    delete_file(tmp_rst)

    return ret_code, node_list_arr


def get_last_log_scn(params, thread, log_is_backed_up):
    encoding = locale.getdefaultlocale()[1]
    pid = params.get("pid")
    instance_name = params.get("instance_name")
    db_user = params.get("db_user")
    db_password = params.get("db_password")

    logger.info(f"Get database node list, instance name:{instance_name}, user_name:{db_user}, pid:{pid}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_last_log_scn_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_last_log_scn_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set linesize 300;\n"
    if thread:
        sql_cmd += f"select to_char(max(next_change#)) from v$archived_log where {log_is_backed_up} " \
                   f"and THREAD#={thread};\n"
    else:
        sql_cmd += f"select to_char(max(next_change#)) from v$archived_log where {log_is_backed_up};\n"
    sql_cmd += f"SPOOL OFF\n" \
               f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)

    db_params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, db_params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error(f"Exec SQL to Get From Scn failed, pid:{pid}, ret_code:{ret_code}, "
                     f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code, []
    content = read_result_file(tmp_rst, encoding=encoding)
    from_scn = content.strip()
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"Get last archive log scn is {from_scn}, pid:{pid}.")

    return get_log_time(params, thread, log_is_backed_up, from_scn)


def get_log_time(params, thread, log_is_backed_up, from_scn):
    encoding = locale.getdefaultlocale()[1]
    pid = params.get("pid")
    instance_name = params.get("instance_name")
    db_user = params.get("db_user")
    db_password = params.get("db_password")
    get_log_time_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_log_time_sql_{pid}.sql"
    get_log_time_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_log_time_rst_{pid}.txt"
    sql_cmd = f"SPOOL {get_log_time_rst}\n" \
              f"set linesize 300;\n"
    if thread:
        sql_cmd += f"select to_char(next_time, {TIME_FORMAT}) from v$archived_log where NEXT_CHANGE#={from_scn} and " \
                   f"{log_is_backed_up} and THREAD#={thread};\n"
    else:
        sql_cmd += f"select to_char(next_time, {TIME_FORMAT}) from v$archived_log where NEXT_CHANGE#={from_scn} and " \
                   f"{log_is_backed_up};\n"
    sql_cmd += "SPOOL OFF;\n" \
               "exit;\n"
    write_tmp_file(get_log_time_sql, sql_cmd)
    set_db_silence_sql(get_log_time_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, get_log_time_sql, get_log_time_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error(f"Execute SQL to Get NEXT_TIME failed, pid:{pid}, ret:{ret_code}, "
                     f"error is:{read_result_file(get_log_time_rst, encoding=encoding)}.")
        delete_file(get_log_time_sql)
        delete_file(get_log_time_rst)
        return ret_code, []
    content = read_result_file(get_log_time_rst, encoding=encoding)
    log_timestamp = content.strip()
    delete_file(get_log_time_sql)
    delete_file(get_log_time_rst)
    logger.info(f"Get last archive log time is {log_timestamp}, pid:{pid}.")

    return ret_code, [from_scn, log_timestamp]


def set_spfile_flag(pid, instance_name, db_user, db_password):
    ret_code, spfile_info = get_dbfiles(pid, instance_name, db_user, db_password, 1)
    if ret_code != ScriptExitCode.SUCCESS or not spfile_info:
        logger.error(f"Get spfile file info failed. pid:{pid}.")
        logger.info(f"Start_with_spfile_flag: False, pid:{pid}")
        return False
    logger.info(f"Start_with_spfile_flag: True, pid:{pid}")
    return True


def get_dbfiles(pid, instance_name, db_user, db_password, file_type):
    encoding = locale.getdefaultlocale()[1]
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/oracle_dbfiles{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/oracle_dbfiles_rst{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"set linesize 999;\n"
    if file_type == 0:
        logger.info(f"Begin to get control files, pid:{pid}.")
        sql_cmd += f"select name from v$controlfile;\n"
    elif file_type == 1:
        logger.info(f"Begin to get spfile files, pid:{pid}.")
        sql_cmd += f"select VALUE from v$parameter where name='spfile';\n"
    elif file_type == 2:
        logger.info(f"Begin to get data guard configuration files, pid:{pid}.")
        sql_cmd += f"select destination || '/*' from v$archive_dest where destination like '/%';\n"
    else:
        logger.error(f"file type {file_type} is not supported, pid:{pid}.")
        return RETURN_INVALID, ""
    sql_cmd += f"SPOOL OFF;\n" \
               f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)

    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    delete_file(tmp_sql)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error(f"Get oracle files failed, pid:{pid}, ret: {ret_code}, "
                     f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_rst)
        return RETURN_INVALID, ""
    content = read_result_file(tmp_rst, encoding=encoding)
    delete_file(tmp_rst)
    return ret_code, content.strip()


def get_online_redo_log_info(pid, instance_name, db_user, db_password, rst_file):
    get_online_log_tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_online_log_info_{pid}.sql"
    sql_cmd = f"SPOOL {rst_file}\n" \
              f"set linesize 999;\n" \
              "col MEMBER for a255;\n" \
              "select GROUP#, MEMBER from v$logfile order by GROUP#;\n" \
              "SPOOL OFF;\n" \
              "exit;\n"
    write_tmp_file(get_online_log_tmp_sql, sql_cmd)
    set_db_silence_sql(get_online_log_tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, get_online_log_tmp_sql, rst_file, instance_name)
    delete_file(get_online_log_tmp_sql)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.info(f"Get oracle online log file failed, pid: {pid}, ret: {ret_code}, "
                    f"error is: {read_result_file(rst_file, encoding=encoding)}.")
        return ret_code
    logger.info(f"Get oracle online log file success, pid: {pid}.")
    return ret_code


def get_db_pwd_file(pid, oracle_home, db_name):
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/oracledb_{pid}.rst"
    db_pw_file = ''

    os.environ['ORACLE_HOME'] = oracle_home
    cmd = f"cmd /c srvctl config database -d {db_name}"
    ret_code, result = execute_windows_cmd(pid, cmd.split(), "")
    if not ret_code:
        logger.info(f"Execute get db pwd file cmd failed, pid: {pid}.")
        return db_pw_file

    write_tmp_file(tmp_rst, result)
    with open(tmp_rst, "r") as file:
        for line in file:
            if "Password file" in line or "口令文件" in line:
                content = line.split(":", 1)
                db_pw_file = content[1].strip()
                break

    delete_file(tmp_rst)
    logger.info(f"Execute get db_pwd_file cmd success, pid: {pid}, db_pwd_file:{db_pw_file}.")

    return db_pw_file


def generate_additional_info(pid, additional, params):
    oracle_home = params.get("oracle_home")
    db_name = params.get("db_name")
    db_instance = params.get("db_instance")
    asm_id_name = params.get("asm_id_name")

    db_pwd_file = get_db_pwd_file(pid, oracle_home, db_name)
    if not db_pwd_file:
        shutil.copy2(f"{oracle_home}\\database\\PWD{db_instance}.ora", f"{additional}\\dbs\\PWD{db_name}.ora")
    elif db_pwd_file[0] == '+':
        os.environ['ORACLE_SID'] = asm_id_name
        os.environ['ORACLE_HOME'] = oracle_home
        orapw_file = f"{PluginPathConstant.WINDOWS_TMP_PATH}/PWD{db_name}.ora"
        if os.path.exists(orapw_file):
            delete_file(orapw_file)

        copy_orapw_cmd = f"cmd /c asmcmd pwcopy {db_pwd_file} {orapw_file}"
        ret_code, result = execute_windows_cmd(pid, copy_orapw_cmd.split(), "")
        if ret_code:
            logger.info(f"Copy orapwdfile success, pid:{pid}.")

        logger.info(f"Touch asm tag, pid: {pid}.")
        if not os.path.exists(f"{additional}\\dbs\\asmtag"):
            create_empty_file(f"{additional}\\dbs\\asmtag")
        if not os.path.exists(f"{additional}\\dbs\\PWD{db_name}.ora"):
            shutil.move(orapw_file, f"{additional}\\dbs")
    else:
        shutil.copy2(f"{db_pwd_file}", f"{additional}\\dbs\\PWD{db_name}.ora")

    src_net_admin = f"{oracle_home}\\network\\admin"
    dest_net_admin = f"{additional}\\netadmin"
    create_dir_recursive(dest_net_admin)
    shutil.copytree(src_net_admin, dest_net_admin, dirs_exist_ok=True)
    shutil.copystat(src_net_admin, dest_net_admin)
    logger.info(f"Success generate additional info, pid:{pid}.")


def get_dir_file_info(pid, main_backup_path, file_list):
    if os.path.exists(file_list):
        delete_file(file_list)
    dir_path_length = len(main_backup_path)
    file_info = {}
    logger.info(f"Start get dir {main_backup_path} file info, pid: {pid}.")
    for dir_path, _, files in os.walk(main_backup_path):
        for name in files:
            if not os.path.isdir(name):
                full_path = os.path.join(dir_path, name)
                file_size = os.path.getsize(full_path)
                full_path = full_path[dir_path_length:]
                if '.snapshot' not in full_path:
                    file_info[full_path] = file_size

    content = f""
    for path, size in file_info.items():
        content += f"{path};{size}\n"
    write_tmp_file(file_list, content)


def change_archive_log_unavailable(pid, params, enc_type, archive, backup_type):
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/change_archive_log_unavailable_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/change_archive_log_unavailable_rst_{pid}.txt"

    sql_cmd = ""
    if backup_type == "log":
        sql_cmd += f"change archivelog like '{archive}\\resetlogs_id%\\arch_%.log' unavailable;\n"
    else:
        sql_cmd += f"change archivelog like '{PluginPathConstant.WINDOWS_ORACLE_DATA_FILE_PATH}%\\log\\arch_%.log' " \
                   f"unavailable;"
    sql_cmd = sql_cmd.upper()
    write_tmp_file(tmp_sql, sql_cmd)

    logger.info(f"Running RMAN to change backup archive logs unavailable, pid:{pid}.")
    ret_code = execute_windows_rman_cmd(pid, params, enc_type, tmp_sql, tmp_rst)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.error(f"Change database-{params.get('instance_name')} log unavailable failed, pid:{pid}, "
                     f"code is:{ret_code}, error is:{read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        if ret_code == ERROR_SCRIPT_EXEC_FAILED:
            return ScriptExitCode.ERROR_ORACLE_BACKUP_FAILED
        return ret_code

    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"Change database-{params.get('instance_name')} log unavailable success, pid: {pid}.")
    return ScriptExitCode.SUCCESS


def get_first_change(params, tmp_sql, tmp_rst, cmd_param):
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    node = cmd_param.get("node", "")
    resetlogs_id = cmd_param.get("resetlogs_id", "")
    from_scn = cmd_param.get("from_scn", "")

    sql_cmd = f"select to_char(first_change#), to_char(next_change#), name from v$archived_log " \
              f"where name is not null " \
              f"and name not like '/mnt/advbackup/%' and THREAD#={node} and RESETLOGS_ID={resetlogs_id} " \
              f"and (deleted = 'NO') and (ARCHIVED='YES') and (STATUS='A') and next_change# > {from_scn};"
    content = f"SPOOL {tmp_rst}\n" \
              f"set linesize 999\n" \
              f"COL name FORMAT a500\n" \
              f"COL DB_UNIQUE_NAME FORMAT a20\n" \
              f"{sql_cmd}\n" \
              f"SPOOL OFF" \
              f"exit"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)
    temp_params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }
    ret_code = execute_windows_sqlplus_cmd(pid, temp_params, tmp_sql, tmp_rst, instance_name)
    return ret_code


def check_archive_log_lost(params, from_scn, resetlogs_id, node_list):
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")

    logger.info(f"check_archive_log_lost, pid: {pid}, database instance name: {instance_name}, user name: {db_user}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_node_list_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_database_node_list_{pid}.txt"

    for node in node_list:
        cmd_param = {
            "node": node,
            "resetlogs_id": resetlogs_id,
            "from_scn": from_scn
        }
        ret_code = get_first_change(params, tmp_sql, tmp_rst, cmd_param)
        if ret_code != ScriptExitCode.SUCCESS:
            logger.info("CheckLogLost failed, ret = %d; pid = %s", ret_code, pid)
            delete_file(tmp_sql)
            delete_file(tmp_rst)
            return ret_code

        # 读取结果文件
        encoding = locale.getdefaultlocale()[1]
        content = read_result_file(tmp_rst, encoding=encoding)
        new_content = content.strip()
        if not new_content:
            logger.info("get_first_change is empty")
            return ScriptExitCode.ERROR_ORACLE_HAS_LOG_LOST
        first_change = new_content.split()[0]
        logger.info("first_change = %s, from_scn = %s", first_change, from_scn)
        if int(first_change) > int(from_scn):
            logger.error(
                "node(%s) has log lost. first change(%s) > from_scn(%s). Backup failed. pid = %s",
                node, first_change, from_scn, pid)
            delete_file(tmp_sql)
            delete_file(tmp_rst)
            first_change_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/check_archive_log_first_change_{pid}.txt"
            write_tmp_file(first_change_rst, first_change)
            return ScriptExitCode.ERROR_ORACLE_HAS_LOG_LOST

    delete_file(tmp_sql)
    delete_file(tmp_rst)
    return ScriptExitCode.SUCCESS


def get_archive_log_num(params, from_scn, archive_path):
    pid = params.get("pid", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")
    instance_name = params.get("instance_name", "")

    logger.info("get_archive_log_num start! pid = %s", pid)
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_archive_log_num_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/get_archive_log_num_sql_{pid}.txt"

    content = f"SPOOL {tmp_rst}\n" \
              f"set linesize 999\n" \
              f"select count(name) from v$archived_log where name is not null and name " \
              f"not like '%{archive_path}%' and next_change# > {from_scn};\n" \
              f"SPOOL OFF" \
              f"exit"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error("GetArchiveLogNum failed, ret = %d, pid = %s", ret_code, pid)
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return 2
    encoding = locale.getdefaultlocale()[1]
    num = read_result_file(tmp_rst, encoding=encoding).strip()
    num = int(num) + 2
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    return num


def backup_windows_log(pid, log_backup_sql_cmd, params, enc_type):
    logger.info(f"Function backup_windows_log start! Pid = {pid}")
    instance_name = params.get("instance_name")

    backup_archive_log_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/backup_archive_log_sql_{pid}.sql"
    backup_archive_log_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/backup_archive_log_sql_{pid}.txt"
    write_tmp_file(backup_archive_log_sql, log_backup_sql_cmd)

    ret = execute_windows_rman_cmd(pid, params, enc_type, backup_archive_log_sql, backup_archive_log_rst)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Backup database-%s archive log failed, code is: %d. pid = %s", instance_name, ret, pid)
        delete_file(backup_archive_log_sql)
        delete_file(backup_archive_log_rst)
        if ret == ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED:
            return ScriptExitCode.ERROR_ORACLE_BACKUP_FAILED
        return ret
    delete_file(backup_archive_log_sql)
    delete_file(backup_archive_log_rst)
    logger.info("Backup database-%s archive log succ. pid = %s", instance_name, pid)
    return ret


def delete_archive_log(pid, delete_end_scn, params):
    instance_name = params.get("instance_name", "")

    logger.info("delete_archive_log start! pid = %s", pid)
    ret_code, archive_dest_list = get_archive_dest_list(pid, params)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error(f"Get archive dest list failed.")
        return ret_code

    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/delete_archive_log_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/delete_archive_log_{pid}.txt"

    content = ""
    for arch_dest in archive_dest_list:
        if arch_dest.startswith("+", SliceConstant.HEAD_OF_STRING_OR_LIST):
            content += f"delete force noprompt archivelog until scn {delete_end_scn} like '{arch_dest}/%';\n".upper()
        else:
            content += f"delete force noprompt archivelog until scn {delete_end_scn} like '{arch_dest}\\%';\n".upper()
    content += "exit;"
    write_tmp_file(tmp_sql, content)

    logger.info("Exec RMAN to delete archivelog of database.")

    ret = execute_windows_rman_cmd(pid, params, 0, tmp_sql, tmp_rst)
    if ret != ScriptExitCode.SUCCESS:
        logger.error(f"Delete archivelog of database{instance_name} failed, pid:{pid}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        if ret == ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED:
            return ScriptExitCode.ERROR_ORACLE_TRUNCATE_ARCHIVELOG_FAILED
        else:
            return ret
    logger.info(f"Delete archivelog of database{instance_name} succ. pid:{pid}.")
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    return ScriptExitCode.SUCCESS


def get_valid_destination(pid, params, tmp_sql, tmp_rst):
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    content = f"set linesize 300;\n" \
              f"col DESTINATION for a255;\n" \
              f"SPOOL {tmp_rst}\n" \
              f"select DESTINATION from v$archive_dest where STATUS='VALID';\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)

    temp_params = {
        "is_silence": 1,
        "db_user": db_user,
        "db_password": db_password
    }
    ret = execute_windows_sqlplus_cmd(pid, temp_params, tmp_sql, tmp_rst, instance_name)
    return ret


def get_recovery_file_dest_name(pid, params, tmp_sql, tmp_rst):
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    content = f"set linesize 300;\n" \
              f"col NAME for a255;\n" \
              f"SPOOL {tmp_rst}\n" \
              f"select NAME from v$RECOVERY_FILE_DEST;\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)
    temp_params = {
        "is_silence": 1,
        "db_user": db_user,
        "db_password": db_password
    }
    logger.info("Exec SQL to get name of archive dest. pid = %s", pid)
    ret = execute_windows_sqlplus_cmd(pid, temp_params, tmp_sql, tmp_rst, instance_name)
    return ret


def get_archive_dest_list(pid, params):
    encoding = locale.getdefaultlocale()[1]
    arch_dest_list = []

    logger.info("get_archive_dest_list start! pid = %s", pid)
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/archive_dest_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/archive_dest_sql_{pid}.txt"

    ret = get_valid_destination(pid, params, tmp_sql, tmp_rst)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Get Archive log dest list failed. pid = %s", pid)
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret, arch_dest_list
    tmp_line = read_result_file(tmp_rst, encoding=encoding).strip()
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    for line in tmp_line.splitlines():
        if line == "USE_DB_RECOVERY_FILE_DEST":
            ret = get_recovery_file_dest_name(pid, params, tmp_sql, tmp_rst)
            if ret != ScriptExitCode.SUCCESS:
                logger.error("Get RECOVERY_FILE_DEST failed, ret = %d. pid = %s", ret, pid)
                delete_file(tmp_sql)
                delete_file(tmp_rst)
                return ret, arch_dest_list
            archived_dest = read_result_file(tmp_rst, encoding=encoding).strip()
            logger.info("STRARCHIVEDEST=%s", archived_dest)
            arch_dest_list.append(archived_dest)
            delete_file(tmp_sql)
            delete_file(tmp_rst)
        elif line:
            arch_dest_list.append(line)
        else:
            logger.warning("get line is empty, continue")
    logger.info(f"Get Archive Dest List success,arch_dest_list={arch_dest_list}, pid: {pid}.")
    return ScriptExitCode.SUCCESS, arch_dest_list


def get_archivelog_current_range(params, archive, resetlogs_id, from_scn):
    # archive是self.LogPath
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    tmp_path = os.path.join(f"{archive}\\resetlogs_id{resetlogs_id}", "log_range")
    if os.path.isfile(tmp_path):
        os.rename(tmp_path, tmp_path + ".bak")
    else:
        write_tmp_file(tmp_path + ".bak", "")

    ret, node_list = get_database_node_list(pid, instance_name, db_user, db_password)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Get node list failed. ret = %d, pid = %s", ret, pid)
        return ret

    log_is_backed_up = f"(RESETLOGS_ID='{resetlogs_id}') and (deleted = 'NO') " \
                       f"and (ARCHIVED='YES') and (STATUS != 'U') and " \
                       f"name like '{archive}\\resetlogs_id{resetlogs_id}\\arch_%' and next_change# > {from_scn}"

    log_is_backed_up = log_is_backed_up.upper()
    write_tmp_file(tmp_path + ".bak", "")
    content = ""
    for node in node_list:
        ret_code, scn_range = get_archivelog_scn_range_by_thread(params, log_is_backed_up, node)
        logger.info("resetlogs_id = %s, node = %s, new scnRange = %s", resetlogs_id, node, scn_range)
        if not scn_range:
            continue
        content += f"node{node}={scn_range}\n"
    logger.info(f"get_archivelog_current_range content={content}")
    write_tmp_file(tmp_path + ".bak", content)
    os.rename(tmp_path + ".bak", tmp_path)
    return ScriptExitCode.SUCCESS


def generate_sequence_info(params, tmp_sql, tmp_rst, thread, log_is_backed_up):
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    content = f"set linesize 300;\n" \
              f"col to_char(min(first_change#)) for a30;\n" \
              f"col to_char(max(next_change#)) for a30;\n" \
              f"SPOOL {tmp_rst}\n" \
              f"select min(sequence#), max(sequence#), to_char(min(first_change#)), to_char(max(next_change#)), " \
              f"to_char(min(first_time), {TIME_FORMAT}), to_char(max(next_time), {TIME_FORMAT}) " \
              f"from v$archived_log where {log_is_backed_up} and THREAD#={thread};\n" \
              f"SPOOL OFF\n" \
              f"exit\n"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)
    params = {
        "is_silence": 1,
        "db_user": db_user,
        "db_password": db_password
    }
    ret = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Get archived log range failed. ret = %d, pid = %s", ret, pid)
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret
    return ret


def generate_change_info(params, tmp_sql, tmp_rst, thread, log_is_backed_up):
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    content = f"set linesize 600;\n" \
              f"SPOOL {tmp_rst}\n" \
              f"select * from ( select to_char(t.next_change#) minscn, " \
              f"to_char(lead(t.first_change#,1,0) over(order by t.sequence#)) maxscn, " \
              f"t.next mintime, to_char(lead(t.first,1,0) over(order by t.sequence#)) maxtime, " \
              f"to_char(lead(t.sequence#,1,0) over(order by t.sequence#)-t.sequence#-1) bb " \
              f"from (select sequence#, first_change#, next_change#, " \
              f"to_char(first_time, 'YYYY-MM-DD_HH24:MI:SS') first, to_char(next_time, 'YYYY-MM-DD_HH24:MI:SS') next " \
              f"from v$archived_log where {log_is_backed_up} and THREAD#={thread})t ) where bb>0;\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    set_db_silence_sql(tmp_sql)

    params = {
        "is_silence": 1,
        "db_user": db_user,
        "db_password": db_password
    }
    ret = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Get archived log range info failed. ret = %d, pid = %s", ret, pid)
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret
    return ret


def determine_missing_num(missing_num, sequences, tmp_rst):
    min_sequence, max_sequence, min_scn, max_scn, min_time, max_time = sequences.split()[0:6]
    if missing_num == 0:
        scn_range = f"{min_scn}~{min_time}~{max_scn}~{max_time}"
    else:
        scn_range = f"{min_scn}~{min_time}"
        index = 0
        encoding = locale.getdefaultlocale()[1]
        result_content = read_result_file(tmp_rst, encoding=encoding).splitlines()
        while index < missing_num:
            temp_line_list = result_content[index].split()
            t_begin = f"{temp_line_list[0]}~{temp_line_list[2]}"
            t_end = f"{temp_line_list[1]}~{temp_line_list[3]}"
            if index == missing_num - 1:
                scn_range = f"{scn_range}~{t_begin} {t_end}~{max_scn}~{max_time}"
            else:
                scn_range = f"{scn_range}~{t_begin} {t_end}"
            index += 1
    return scn_range


def get_archivelog_scn_range_by_thread(params, log_is_backed_up, thread):
    encoding = locale.getdefaultlocale()[1]
    scn_range = ""
    pid = params.get("pid", "")
    instance_name = params.get("instance_name", "")
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    logger.info("get_archiveLog_scn_range_by_thread start! pid = %s", pid)
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/trunCateLog{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/trunCateLog{pid}.txt"
    ret_code = generate_sequence_info(params, tmp_sql, tmp_rst, thread, log_is_backed_up)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.info("Generate sequence info FAILED! pid = %s", pid)
        return ret_code, scn_range
    delete_file(tmp_sql)
    sequences = read_result_file(tmp_rst, encoding=encoding)
    logger.info(f"sequences:{sequences}.")
    min_sequence, max_sequence, min_scn, max_scn, min_time, max_time = sequences.split()[0:6]
    logger.info(f"Node [{thread}] sequenceRange={min_sequence}~{max_sequence}, "
                f"scnRange={min_scn}~{max_scn}, timeRange={min_time}~{max_time}")
    delete_file(tmp_rst)

    ret_code = generate_change_info(params, tmp_sql, tmp_rst, thread, log_is_backed_up)
    if ret_code != ScriptExitCode.SUCCESS:
        logger.error("Get archived log range info failed. ret = %d, pid = %s", ret_code, pid)
        return ret_code, scn_range
    rst_content = read_result_file(tmp_rst, encoding=encoding).splitlines()
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    missing_num = 0
    for line in rst_content:
        if not line or (line == "no rows selected"):
            continue
        missing_num += 1
    logger.info("missing_num = %d", missing_num)
    scn_range = determine_missing_num(missing_num, sequences, tmp_rst)
    ret, db_role = get_db_role(pid, db_user, db_password, instance_name)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Get db_role failed. pid = %s", pid)
        return ret, scn_range
    logger.info(f"db_role={db_role},min_sequence={min_sequence},max_sequence={max_sequence}")
    if db_role == "PRIMARY":
        if min_sequence == max_sequence:
            scn_range = ""

    return ScriptExitCode.SUCCESS, scn_range


def get_db_role(pid, db_user, db_password, instance_name):
    database_role = ""

    db_role_tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/database_role_{pid}.sql"
    db_role_tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/database_role_{pid}.rst"

    content = f"SPOOL {db_role_tmp_rst}\n" \
              f"select database_role from v$database;\n" \
              f"SPOOL OFF\n" \
              f"exit;\n"
    write_tmp_file(db_role_tmp_sql, content)
    set_db_silence_sql(db_role_tmp_sql)

    params = {
        "is_silence": 1,
        "db_user": db_user,
        "db_password": db_password
    }
    ret = execute_windows_sqlplus_cmd(pid, params, db_role_tmp_sql, db_role_tmp_rst, instance_name)
    if ret != ScriptExitCode.SUCCESS:
        logger.error("Query database role failed. ret = %d, pid = %s", ret, pid)
        delete_file(db_role_tmp_sql)
        delete_file(db_role_tmp_rst)
        return ret, database_role

    encoding = locale.getdefaultlocale()[1]
    database_role = read_result_file(db_role_tmp_rst, encoding=encoding).strip()
    logger.info("Database role = %s", database_role)
    delete_file(db_role_tmp_sql)
    delete_file(db_role_tmp_rst)
    return ScriptExitCode.SUCCESS, database_role


def get_all_archivelog_range(pid, archive):
    logger.info("Begin GetArchiveLogRange. pid = %s", pid)
    encoding = locale.getdefaultlocale()[1]
    log_backup_rst = "logbackuprst"

    os.chdir(archive)
    all_dirname_list = os.listdir()
    dir_name_list = []
    for dirname in all_dirname_list:
        if dirname.startswith("resetlogs_id", SliceConstant.HEAD_OF_STRING_OR_LIST) and os.path.isdir(dirname):
            dir_name_list.append(dirname)

    for dir_name in dir_name_list:
        resetlogs_id = dir_name[12:]
        if os.path.isfile(f"{archive}\\resetlogs_id{resetlogs_id}\\log_range.bak"):
            os.remove(f"{archive}\\resetlogs_id{resetlogs_id}\\log_range.bak")
        tmp_content = read_result_file(f"{archive}\\resetlogs_id{resetlogs_id}\\log_range", encoding=encoding)
        if (not os.path.isfile(f"{archive}\\resetlogs_id{resetlogs_id}\\log_range")) \
                or (not tmp_content):
            continue
        result_range = []
        log_range_content = read_result_file(f"{archive}\\resetlogs_id{resetlogs_id}\\log_range",
                                             encoding=encoding).splitlines()
        logger.info(f"log_range_content:{log_range_content}")
        for line in log_range_content:
            node = line.split("=")[0]
            scn_range = line.split("=")[1]
            logger.info(f"line={line}, node={node}, scn_range={scn_range}")
            if not scn_range:
                continue
            if not result_range:
                result_range = [scn_range]
            else:
                result_range = calintersection_log_range(result_range, [scn_range])
        logger.info("get result_range = %s", result_range)
        for word in result_range:
            min_scn = word.split("~")[0]
            min_time = word.split("~")[1]
            min_timestamp = add_unix_timestamp(min_time)
            max_scn = word.split("~")[2]
            max_time = word.split("~")[3]
            max_timestamp = add_unix_timestamp(max_time)
            log_backup_rst = f"{log_backup_rst};{min_scn};{min_timestamp};{max_scn};{max_timestamp};{resetlogs_id}"

    logger.info("log_backup_rst = %s", log_backup_rst)
    return log_backup_rst


def calintersection_log_range(before_range, after_range):
    c_result_range = ""
    for word_a in before_range:
        before_min_scn = word_a.split("~")[0]
        before_min_time = word_a.split("~")[1]
        before_max_scn = word_a.split("~")[2]
        before_max_time = word_a.split("~")[3]
        for word_b in after_range:
            after_min_scn = word_b.split("~")[0]
            after_min_time = word_b.split("~")[1]
            after_max_scn = word_b.split("~")[2]
            after_max_time = word_b.split("~")[3]
            if (before_min_scn > after_max_scn) or (after_min_scn > before_max_scn):
                continue

            if before_min_scn > after_min_scn:
                c_result_range = f"{c_result_range} {before_min_scn}~{before_min_time}"
            else:
                c_result_range = f"{c_result_range} {after_min_scn}~{after_min_time}"

            if before_max_scn > after_max_scn:
                c_result_range = f"{c_result_range}~{after_max_scn}~{after_max_time}"
            else:
                c_result_range = f"{c_result_range}~{before_max_scn}~{before_max_time}"
    return [c_result_range] if c_result_range else []


def build_backup_archive_log_sql(params):
    is_enc_bk = params.get("is_enc_bk")
    ora_pre_version = int(params.get("ora_pre_version"))
    channels = params.get("channels")
    archive = params.get("archive")
    resetlogs_id = params.get("resetlogs_id")
    open_mode = params.get("open_mode")
    from_scn = params.get("from_scn")
    db_name = params.get('db_name')

    cmd = f"configure backup optimization off;\n" \
          f"configure controlfile autobackup off;\n" \
          f"set nocfau;\n" \
          f"configure maxsetsize to unlimited;\n"
    if is_enc_bk == 0 and ora_pre_version > 11:
        cmd += f"configure encryption for database off;\n"
    cmd += "RUN {\n"
    cmd += f"    SET COMMAND ID TO 'ProtectAgent_Archive_Backup';\n"

    cmd += build_archive_log_qos_sql(params)
    cmd += f"    configure device type disk parallelism {channels};\n"
    if open_mode != "READONLY" and open_mode != "READONLYWITHAPPLY":
        cmd += "    sql 'alter system archive log current';\n"
    cmd += f"    backup as copy archivelog from scn {from_scn} format '{archive}\\resetlogs_id" \
           f"{resetlogs_id}\\arch_%t_%s.log';\n"
    # 备份控制文件
    cmd += f"    backup as copy current controlfile format '{archive}\\resetlogs_id{resetlogs_id}\\controlfile.ctl' " \
           f"tag 'EBACKUP-{db_name}-CTL' reuse;\n"
    cmd += "}"

    return cmd


def build_archive_log_qos_sql(params):
    qos = params.get("qos")
    log_num = params.get("log_num")
    db_is_cluster = params.get("db_is_cluster")
    node_number = params.get("node_number")
    ip_port_info = params.get("ip_port_info")
    channels = params.get("channels")
    archive = params.get("archive")
    resetlogs_id = params.get("resetlogs_id")

    cmd = ""
    real_qos = 0
    if qos > 0:
        if log_num >= channels or log_num == 0:
            real_qos = qos * 1024 // channels
        else:
            real_qos = qos * 1024 // log_num
    index = 1
    if db_is_cluster != 1 or node_number == 0:
        while index <= channels:
            qos_sql = f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk" \
                      f" format '{archive}\\resetlogs_id{resetlogs_id}\\%T_%U'"
            if qos > 0:
                cmd += f"{qos_sql} rate {real_qos}k;\n"
            else:
                cmd += f"{qos_sql};\n"
            index += 1
        return cmd

    while index <= channels:
        for instance_node_info in ip_port_info:
            [host_info, port_info, sid_info] = instance_node_info.split(",")
            qos_sql = f"    allocate channel eBackup{'{:0>2d}'.format(index)} type disk" \
                      f" format '{archive}\\resetlogs_id{resetlogs_id}\\%T_%U'"
            if qos == 0:
                cmd += f"{qos_sql} connect /@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host={host_info})(Port=" \
                       f"{port_info}))(CONNECT_DATA=(SID={sid_info})));\n"
            else:
                cmd += f"{qos_sql} rate {real_qos}k connect /@(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)(Host=" \
                       f"{host_info})(Port={port_info}))(CONNECT_DATA=(SID={sid_info})));\n"
            index += 1
            if index > channels:
                break
    return cmd


def create_archive_log(pid, instance_name, db_user, db_password):
    logger.info(f"Create archive log, pid: {pid}, instance name: {instance_name}, user name: {db_user}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/create_archive_log_sql_{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/create_archive_log_rst_{pid}.txt"
    sql_cmd = f"SPOOL {tmp_rst}\n" \
              f"alter system archive log current;\n" \
              f"SPOOL OFF;\n" \
              f"exit;\n"
    write_tmp_file(tmp_sql, sql_cmd)
    set_db_silence_sql(tmp_sql)
    params = {
        'is_silence': 1,
        'db_user': db_user,
        'db_password': db_password
    }

    ret_code = execute_windows_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, instance_name)
    if ret_code != ScriptExitCode.SUCCESS:
        encoding = locale.getdefaultlocale()[1]
        logger.info(f"Create archive log failed, pid:{pid}, ret:{ret_code}, "
                    f"error is: {read_result_file(tmp_rst, encoding=encoding)}.")
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        return ret_code

    logger.info(f"Create archive log success, pid:{pid}.")
    # 删除产生的临时文件
    delete_file(tmp_sql)
    delete_file(tmp_rst)

    return ret_code


def validate_low_auth_list(auth_list: list):
    """
    输入低权限用户列表检验所有低权限用户名均不为空
    :param auth_list: 低权限用户列表
    """
    for low_auth_info in auth_list:
        if not low_auth_info.get("low_auth_user", ""):
            # 任何低权限用户名均为必填项，如果检测为空，直接任务失败
            return ScriptExitCode.ERROR_INVALID_LOW_AUTH_INFO
    return ScriptExitCode.SUCCESS


def get_low_auth_info(pid: str):
    """
    找到对应本机ip的低权限用户信息
    :param pid: 任务id
    """
    target_low_auth = {}

    auth_list = get_low_auth_info_from_env()

    # 获取本机ip
    cmd_list = ["ipconfig"]
    ret, result = execute_windows_cmd(pid, cmd_list, "")
    if not ret:
        logger.error(f"Get ip FAILED! PID = {pid}")
        return ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED, target_low_auth

    target_ip_list = []
    for line in result.splitlines():
        if line.strip().startswith("IPv4"):
            current_ip = line.strip().split(":")[-1].strip()
            target_ip_list.append(current_ip)

    # 在低权限用户列表中找到对应主机ip的信息
    for low_auth_info in auth_list:
        temp_ip = low_auth_info.get("ip", "")
        temp_subnet_ip = low_auth_info.get("subNetFixedIp", "")
        if (temp_ip in target_ip_list) or (temp_subnet_ip in target_ip_list):
            target_low_auth = low_auth_info
            return ScriptExitCode.SUCCESS, target_low_auth
    logger.error(f"None of ip matched the host! PID = {pid}")
    return ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED, target_low_auth


def get_low_auth_info_from_env():
    return json.loads(get_env_variable(EnvVariableKeys.run_user_pwd))


def windows_switch_user(pid):
    ret, low_auth_info = get_low_auth_info(pid)
    if ret != ScriptExitCode.SUCCESS:
        return ret, ""

    username = low_auth_info.get("low_auth_user", "")
    password = low_auth_info.get("password", "")
    domain = ""

    try:
        switch_user = win32security.LogonUser(username, domain, password, win32con.LOGON32_LOGON_NETWORK,
                                              win32con.LOGON32_PROVIDER_DEFAULT)
        win32security.ImpersonateLoggedOnUser(switch_user)
    except pywintypes.error as e:
        logger.error(f"Switch to user {username} FAILED!")
        return ScriptExitCode.ERROR_INVALID_LOW_AUTH_INFO, ""
    finally:
        clear(password)

    logger.info(f"Switch to user {username} SUCCESS!")
    return ScriptExitCode.SUCCESS, username


def windows_switch_user_end(username):
    try:
        win32security.RevertToSelf()
    except pywintypes.error as e:
        logger.error(f"Switch back from user {username} FAILED!")
        return False

    logger.info(f"Switch back from user {username} SUCCESS!")
    return True


def execute_windows_asm_sqlplus_cmd(pid, params, tmp_sql, tmp_rst, asm_instance_name):
    """
    在windows中执行sqlplus指令，输出结果文件，输出错误具体详情文件，输出错误码
    错误具体详情文件在stmp路径下，以result_errordetail_{pid}的形式保存
    :param pid: 请求id
    :param params: 包括"is_silence"(int: 0, 1)，"db_user"(str)和"db_password"(str)三个字段的字典
    :param tmp_sql: 执行命令语句的.sql文件路径
    :param tmp_rst: 输出的结果文件路径，该路径由tmp_sql里的内容决定
    :param asm_instance_name: asm实例名
    :return: 执行语句的错误码信息，0为成功（不输出错误具体详情文件）
    """
    # 解析params信息
    db_user = params.get("db_user", "")
    db_password = params.get("db_password", "")

    if db_password:
        db_password = f'"{db_password}"'

    # 执行命令
    os.environ["ORACLE_SID"] = asm_instance_name
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    cmd_list = f'sqlplus /nolog'.split()
    input_cmd = f'conn {db_user}/{db_password} as sysasm\n' \
                f'@{tmp_sql}\n'

    ret, result = execute_windows_cmd_with_low_auth_user(pid, cmd_list, input_cmd)

    # 输出错误码以及错误详情文件
    if not ret or ("ERROR" in result):
        logger.error("Execute oracle sqlplus cmd failed. pid = %s", pid)
        result_code = ScriptExitCode.ERROR_SCRIPT_EXEC_FAILED
        get_sql_error_info(pid, tmp_rst, "sqlplus", result_code)
        return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
    return ScriptExitCode.SUCCESS


def modify_spfile_path_windows(pid, db_instance_name, spfile_path):
    logger.info(f"modify_spfile_path_windows , pid: {pid}")
    cmd = f"cmd /c srvctl modify database -d {db_instance_name} -p {spfile_path}"
    ret, output = execute_windows_cmd(pid, cmd.split(), "")
    if not ret:
        logger.error(f"modify_spfile_path_windows failed! cmd ={cmd}, output {output}")
        return False
    logger.info(f"modify_spfile_path_windows success! pid: {pid}")
    return True


def modify_asm_spfile_path_windows(pid, spfile_path):
    cmd = f"cmd /c srvctl modify asm -p {spfile_path}"
    logger.info(f"modify_asm_spfile_path_windows, pid: {pid}, cmd: {cmd}")
    ret, output = execute_windows_cmd(pid, cmd.split(), "")
    if not ret:
        logger.error(f"modify_asm_spfile_path_windows failed! cmd ={cmd}, output {output}")
        return False
    logger.info(f"modify_asm_spfile_path_windows success! pid: {pid}")
    return True


def asm_startup_windows(pid, asm_instance, db_user, db_password):
    logger.info(f"Begin to start asm, pid:{pid}.")
    encoding = locale.getdefaultlocale()[1]
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/StartASM{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/StartASMRST{pid}.txt"
    content = f"SPOOL {tmp_rst}\n" \
              f"startup;\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    silent_params = {
        "db_user": db_user,
        "db_password": db_password,
    }
    ret_code = execute_windows_asm_sqlplus_cmd(pid, silent_params, tmp_sql, tmp_rst, asm_instance)
    content = read_result_file(tmp_rst, encoding=encoding).splitlines()
    if ret_code != ScriptExitCode.SUCCESS:
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        logger.error(f"Start ASM failed, ret={ret_code}, errmsg={content}")
        return False
    for line in content:
        if "ASM instance started" in line or "already-running" in line:
            logger.info(f"ASM instance {asm_instance} is OPEN, pid:{pid}.")
            delete_file(tmp_sql)
            delete_file(tmp_rst)
            return True
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"Start ASM success, pid:{pid}")
    return True


def modify_asm_disk_string_windows(pid, asm_instance, db_user, db_password, asm_disk_string):
    logger.info(f"Begin to modify_asm_disk_string, pid:{pid}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/ModifyASM{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/ModifyASMRST{pid}.txt"
    content = f"SPOOL {tmp_rst}\n" \
              f"alter system set asm_diskstring='{asm_disk_string}';\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    silent_params = {
        "db_user": db_user,
        "db_password": db_password,
    }
    ret_code = execute_windows_asm_sqlplus_cmd(pid, silent_params, tmp_sql, tmp_rst, asm_instance)
    encoding = locale.getdefaultlocale()[1]
    content = read_result_file(tmp_rst, encoding=encoding).splitlines()
    for line in content:
        if "ORA-01565" in line:
            logger.info(f"modify_asm_disk_string ORA-01565, line:{line}.")
            delete_file(tmp_sql)
            delete_file(tmp_rst)
            return True
    if ret_code != ScriptExitCode.SUCCESS:
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        logger.error(f"modify_asm_disk_string failed, ret={ret_code}, errmsg={content}")
        return False
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"modify_asm_disk_string success, pid:{pid}")
    return True


def asmcmd_mount_windows(pid, asm_instance, oracle_home, disk_group):
    cmd = f"cmd /c asmcmd mount {disk_group}"
    os.environ["ORACLE_SID"] = asm_instance
    os.environ["ORACLE_HOME"] = oracle_home
    os.environ["NLS_LANG"] = "American_America.US7ASCII"
    ret, result = execute_windows_cmd(pid, cmd.split(), "")
    if not ret:
        logger.error(f"asmcmd mount failed, disk_group is {disk_group}, result:{result}")
        return False
    return True


def asm_mount_windows(pid, asm_instance, db_user, db_password, disk_group):
    logger.info(f"Begin to asm_mount, pid:{pid}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/MountASM{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/MountASMRST{pid}.txt"
    content = f"SPOOL {tmp_rst}\n" \
              f"alter diskgroup {disk_group} mount;\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    silent_params = {
        "db_user": db_user,
        "db_password": db_password,
    }
    ret_code = execute_windows_asm_sqlplus_cmd(pid, silent_params, tmp_sql, tmp_rst, asm_instance)
    encoding = locale.getdefaultlocale()[1]
    content = read_result_file(tmp_rst, encoding=encoding).splitlines()
    for line in content:
        if "is already mounted" in line:
            logger.info(f"disk_group already mounted, pid:{pid}.")
            delete_file(tmp_sql)
            delete_file(tmp_rst)
            return True
    if ret_code != ScriptExitCode.SUCCESS:
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        logger.error(f"asm_mount failed, ret={ret_code}, errmsg={content}")
        return False
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"asm_mount success, pid:{pid}")
    return True


def get_grid_home_path():
    # 获取oracle的home_path
    grid_home_path = ""
    registry_list = list_registry_directory("SOFTWARE\\ORACLE")
    for registry in registry_list:
        if registry.startswith("SOFTWARE\\ORACLE\\KEY_OraGI", SliceConstant.HEAD_OF_STRING_OR_LIST):
            logger.info("Get a grid!")
            ret, home_path = get_registry(WindowsRegistryParentDirectory.HKEY_LOCAL_MACHINE, registry,
                                          "ORACLE_HOME")
            if not ret:
                logger.error("Cannot find home_env!")
                return ScriptExitCode.ERROR_SCRIPT_ORACLEHOME_LOST, ""
            grid_home_path = home_path
            break
    if not grid_home_path:
        logger.error("Cannot find Grid information!")
        return RETURN_INVALID, ""
    return ErrorCode.SUCCESS.value, grid_home_path


def start_ora_storage(pid, path: str):
    logger.info("Function start_ora_storage start!")
    home_path = os.path.join(path, "BIN")
    query_cmd = "cmd /c crsctl start res ora.storage -init"
    ret, output = execute_windows_cmd(pid, query_cmd.split(), "", home_path)
    if not ret:
        logger.error(f"start_ora.storage failed, std_out {output}")
        return RETURN_INVALID
    return ErrorCode.SUCCESS.value


def check_crsd_status_win(pid, path: str):
    logger.info("Function check_crsd_status_win start!")
    home_path = os.path.join(path, "BIN")
    query_cmd = "cmd /c crsctl check crs"
    ret, output = execute_windows_cmd(pid, query_cmd.split(), "", home_path)
    if not ret:
        logger.error(f"check_crsd_status_win failed, std_out {output}")
        return False
    if output.__contains__("Cannot communicate with Cluster Ready Services"):
        logger.error(f"check_crsd_status offline")
        return False
    logger.info(f"check_crsd_status online, pid {pid}")
    return True


def start_ora_crsd(pid, path: str):
    logger.info("Function start_ora_crsd start!")
    home_path = os.path.join(path, "BIN")
    query_cmd = "cmd /c crsctl start res ora.crsd -init"
    ret, output = execute_windows_cmd(pid, query_cmd.split(), "", home_path)
    if not ret:
        logger.error(f"start_ora.crsd failed, std_out {output}")
        return RETURN_INVALID
    return ErrorCode.SUCCESS.value


def shutdown_abort_windows(pid, db_instance, db_user, db_password):
    logger.info(f"Begin to shutdown_abort, pid:{pid}.")
    tmp_sql = f"{PluginPathConstant.WINDOWS_TMP_PATH}/SHUTDOWN{pid}.sql"
    tmp_rst = f"{PluginPathConstant.WINDOWS_TMP_PATH}/SHUTDOWNRST{pid}.txt"
    content = f"SPOOL {tmp_rst}\n" \
              f"SHUTDOWN ABORT;\n" \
              f"SPOOL OFF\n" \
              f"exit;"
    write_tmp_file(tmp_sql, content)
    silent_params = {
        "db_user": db_user,
        "db_password": db_password,
    }
    ret_code = execute_windows_sqlplus_cmd(pid, silent_params, tmp_sql, tmp_rst, db_instance)
    encoding = locale.getdefaultlocale()[1]
    content = read_result_file(tmp_rst, encoding=encoding).splitlines()
    if ret_code != ScriptExitCode.SUCCESS:
        delete_file(tmp_sql)
        delete_file(tmp_rst)
        logger.error(f"shutdown_abort failed, ret={ret_code}, errmsg={content}")
        return False
    delete_file(tmp_sql)
    delete_file(tmp_rst)
    logger.info(f"shutdown_abort success, pid:{pid}")
    return True
